/*********************************************************
          File Name:clp.h
          Author: Abby Cin
          Mail: abbytsing@gmail.com
          Created Time: Wed 29 Mar 2017 07:03:38 PM CST
**********************************************************/

#ifndef CLP_H_
#define CLP_H_

#include <fstream>
#include <unordered_map>
#include <stdexcept>
#include <list>
#include <set>
#include <type_traits>
#include "string_ext.h"

namespace nm
{
  template<typename K>
  class Optional
  {
    public:
      Optional(const K& key, bool val)
        : data_(key, val)
      {}

      Optional(const Optional& rhs)
        : data_(rhs.data_)
      {}

      explicit operator bool()
      {
        return data_.second;
      }

      K& value()
      {
        return data_.first;
      }

    private:
      std::pair<K, bool> data_;
  };

  class Clp
  {
    template<typename T, typename U> struct strict_same
    {
      constexpr static bool value = false;
    };
    template<typename T> struct strict_same<T, T>
    {
      constexpr static bool value = true;
    };

    public:
      enum Option
      {
        SHORT = 1,
        LONG = 2,
        ALL = 3
      };

      Clp(int argc, char* argv[]) noexcept
        : map_{}, cmd_{}, c_{}
      {
        for(int i = 1; i < argc; ++i)
          cmd_.emplace_back(argv[i]);
      }

      Clp(const std::string& path)
        : map_{}, cmd_{}, none_{}, c_{}, msg_{}
      {
        errno = 0;
        std::ifstream in(path);
        if(in.is_open())
        {
          string_ext line{};
          while(std::getline(in, line))
          {
            if(line.empty())
              continue;
            line.lstrip();
            if(!line.empty() && line[0] == '#')
              continue;
            line.split(cmd_, " ");
          }
          for(auto& x: cmd_)
          {
            x.strip();
          }
          in.close();
          return;
        }
        msg_ = path + ' ';
        msg_ += strerror(errno);
      }
      
      Clp(const Clp&) = delete;

      Clp(Clp&& rhs) noexcept
        : map_{std::move(rhs.map_)},
          cmd_{std::move(rhs.cmd_)},
          none_{std::move(rhs.none_)},
          c_{std::move(rhs.c_)},
          msg_{std::move(rhs.msg_)}
      {}

      Clp& operator= (const Clp&) = delete;

      ~Clp() noexcept {}

      Clp& parse(Option op)
      {
        if(!msg_.empty())
          return *this;
        msg_.clear();
        switch(op)
        {
          case Option::SHORT:
            parse("-");
            break;
          case Option::LONG:
            parse("--");
            break;
          case Option::ALL:
            parse("-");
            parse("--");
            break;
        }
        return *this;
      }

      bool ok() const noexcept
      {
        return msg_.empty();
      }

      const std::string& msg() const noexcept
      {
        return msg_;
      }

      template<typename T>
      Optional<T> get(const std::string& key) noexcept
      {
        msg_.clear();
        auto iter = map_.find(key);
        if(iter == map_.end())
        {
          msg_ = "no `" + key + "` in argument list";
          return {T{}, false};
        }

        T res = get_value<T>(iter->second);
        if(ok())
          return {res, true};
        return {res, false};
      }

      const std::set<std::string>& none() const noexcept
      {
        return none_;
      }

      bool contain(const std::string& key) const noexcept
      {
        return c_.find(key) != c_.end();
      }

    private:
      std::unordered_map<std::string, std::string> map_;
      std::list<string_ext> cmd_;
      std::set<std::string> none_;
      std::set<std::string> c_;
      std::string msg_;

      void parse(const std::string& delim)
      {
        std::string tmp{};
        bool has_key{false};
        msg_ = "invalid option";
        for(auto iter = cmd_.begin(); iter != cmd_.end(); ++iter)
        {
          has_key = false;
          if(*iter == "-")
          {
            return;
          }
          // get key
          if(iter->find_first_of(delim) != iter->npos && *iter != "--")
          {
            tmp = iter->substr(delim.length());
            has_key = true;
            ++iter;
          }
          if(iter == cmd_.end())
          {
            c_.emplace(tmp);
            break;
          }
          else if(*iter == "-")
          {
            return;
          }
          // get escape
          if(*iter == "--")
          {
            if(!has_key)
            {
              return;
            }
            if(++iter != cmd_.end())
            {
              map_.insert({tmp, *iter});
              c_.emplace(tmp);
            }
            else
            {
              // treat as single option
              c_.emplace(tmp);
              break;
            }
          }
          // no value
          else if(iter->find_first_of(delim) < delim.size())
          {
            --iter;
            c_.emplace(tmp);
          }
          else if(has_key)
          {
            map_.insert({tmp, *iter});
            c_.emplace(tmp);
          }
          else
          {
            none_.emplace(*iter);
          }
        }
        msg_.clear();
      }

      template<typename T>
      T get_value(const std::string& str) noexcept
      {
        try
        {
          if(std::is_floating_point<T>::value)
            return static_cast<T>(std::stold(str));
          else if(std::is_signed<T>::value)
            return static_cast<T>(std::stoll(str));
          else
            return static_cast<T>(std::stoull(str));
        }
        catch(const std::out_of_range&)
        {
          msg_ = "error: out of range";
        }
        catch(const std::invalid_argument&)
        {
          msg_ = "error: invalid argument";
        }
        return {};
      }
  };

  template<> Optional<std::string> Clp::get<std::string>(const std::string& key) noexcept
  {
    msg_.clear();
    auto iter = map_.find(key);
    if(iter == map_.end())
    {
      msg_ = "no `" + key + "` in argument list";
      return {"", false};
    }
    return {iter->second, true};
  }

  template<> Optional<bool> Clp::get<bool>(const std::string& key) noexcept
  {
    msg_.clear();
    auto iter = map_.find(key);
    if(iter == map_.end())
    {
      msg_ = "no `" + key + "` in argument list";
      return {false, false};
    }
    if(iter->second == "true")
      return {true, true};
    else if(iter->second == "false")
      return {false, true};
    else
      msg_ = "invalid argument";
    return {false, false};
  }
}

#endif // CLP_H_
